import re
import os
import hashlib
import base64
from cryptography.fernet import Fernet
import secrets
import select


from .json_helper import JsonHelper


def _key_from_secret(secret: str) -> bytes:
    """A URL-safe base64-encoded 32-byte key"""
    h = hashlib.sha256(('tinyops:' + secret).encode('utf-8'))
    return base64.urlsafe_b64encode(h.digest())


def encrypt(secret: str, value: str) -> str:
    """
    >>> assert encrypt("abc", "xyz")
    """
    key = _key_from_secret(secret)
    data = JsonHelper.dumps(value).encode('utf-8')
    return Fernet(key).encrypt(data).decode('ascii')


def decrypt(secret: str, value: str) -> str:
    """
    >>> s = encrypt("abc", "xyz")
    >>> decrypt("abc", s) == "xyz"
    True
    """
    key = _key_from_secret(secret)
    data = Fernet(key).decrypt(value.encode('ascii'))
    return JsonHelper.loads(data, encoding='utf-8')


def _generate_token(secret: str, value: str, nonce, half_length: int):
    key = hashlib.sha256(('tinyops:' + secret).encode('utf-8')).digest()
    if value:
        key += hashlib.sha256(value.encode('utf-8')).digest()
    digest = hashlib.sha256(key + nonce).digest()[:half_length]
    return (nonce + digest).hex()


def generate_token(secret: str, value: str = None, length=16) -> str:
    """
    >>> len(generate_token('abc'))
    32
    >>> len(generate_token('abc', 'xyz'))
    32
    """
    half_length = length // 2
    nonce = secrets.token_bytes(half_length)
    return _generate_token(secret, value, nonce, half_length)


def verify_token(secret: str, value: str, token: str) -> bool:
    """
    >>> token = generate_token('abc', 'xyz')
    >>> verify_token('abc', 'xyz', token)
    True
    >>> token = generate_token('abc', length=24)
    >>> verify_token('abc', None, token)
    True
    >>> token = generate_token('abc', 'xyz')
    >>> verify_token('abc', '123', token)
    False
    """
    token_bytes = bytes.fromhex(token)
    half_length = len(token_bytes) // 2
    nonce = token_bytes[:half_length]
    mytoken = _generate_token(secret, value, nonce, half_length)
    return secrets.compare_digest(mytoken, token)


def detect_shell(etc_shells_content):
    """
    detech best shell from /etc/shells file

    >>> content = '''
    ... /bin/zsh
    ... /bin/bash
    ... '''
    >>> detect_shell(content)
    '/bin/bash'
    >>> detect_shell('')
    '/bin/sh'
    """
    if isinstance(etc_shells_content, bytes):
        etc_shells_content = etc_shells_content.decode('utf-8')
    available_shells = []
    for line in etc_shells_content.splitlines():
        line = line.strip()
        if line and not line.startswith('#'):
            available_shells.append(line)
    for x in available_shells:
        if x.endswith('/bash'):
            return x
    for x in available_shells:
        if x.endswith('/sh'):
            return x
    if not available_shells:
        return '/bin/sh'
    return available_shells[0]


# https://stackoverflow.com/questions/14693701/how-can-i-remove-the-ansi-escape-sequences-from-a-string-in-python
# https://superuser.com/questions/380772/removing-ansi-color-codes-from-text-stream
# https://stackoverflow.com/questions/5947742/how-to-change-the-output-color-of-echo-in-linux/5947788
RE_ASCII_ESCAPE = re.compile(r"(\x9B|\x1B\[)[0-?]*[ -/]*[@-~]", re.I)


def decode_output(msg):
    r"""
    >>> msg = 'I \x1b[0;31mlove\x1b[0m Stack Overflow'
    >>> decode_output(msg)
    'I love Stack Overflow'
    >>> msg = 'ls\r\n\x1b[00m\x1b[01;31mexamplefile.zip\x1b[00m\r\n\x1b[01;31m'
    >>> decode_output(msg)
    'ls\nexamplefile.zip\n'
    >>> msg = '中文'.encode('gbk')
    >>> decode_output(msg) == str(msg)
    True
    """
    if isinstance(msg, bytes):
        try:
            msg = msg.decode('utf-8')
        except UnicodeDecodeError:
            msg = str(msg)
    msg = RE_ASCII_ESCAPE.sub("", msg)
    # convert dos linefeeds (CRLF) to unix (LF)
    msg = msg.replace('\r\n', '\n')
    return msg


def process_output_stream(fd, timeout=60, buffer_size=1024):
    while True:
        try:
            r, w, e = select.select([fd], [], [], timeout)
        except OSError:
            break
        if not r:
            break
        try:
            out = os.read(fd, buffer_size)
        except OSError:
            break
        else:
            if not out:
                break
            yield decode_output(out)
